﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Common;
using System.Data;
using System.Reflection;

namespace Petaframe.DataAccess
{

    /// <summary>
    /// Veritabanı işlemlerini tanımlayan temel soyut sınıftır. Bu sınıfın örneği oluşturulamaz. Veritabanı
    /// işlemleri için <see cref="SqlServerDataService"/> veya <see cref="OracleDataService"/> kullanılmalıdır.
    /// </summary>
    public abstract class DataService : IDataService 
    {
        internal DbProviderFactory _dbProviderFactory;
        internal DbConnection _dbConnection;
        internal DbCommand _dbCommand;     
        internal DbTransaction _dbTransaction;
        protected List<DbParameter> _dbParameterList;
        private DbDataReader _dbDataReader;
        readonly string _serviceConnectionString;
        int _maxNumberOfResults = 100000;
        private Dictionary<string, object> _outputParameters;
        internal IParameterBuilder _parameterBuilder;
        private IDetachedQuery _detachedQuery;
        internal IEscapeSqlIdentifier _escapeIdentifier;

        public DataService(string serviceConnectionString, IParameterBuilder parameterBuilder, IEscapeSqlIdentifier escapeSqlIdentifier, DbProviderFactory providerFactory)
        {
            _dbParameterList = new List<DbParameter>();
            _serviceConnectionString = serviceConnectionString;
            _outputParameters = new Dictionary<string, object>();           
            _parameterBuilder = parameterBuilder;
            _dbProviderFactory = providerFactory;
            _escapeIdentifier = escapeSqlIdentifier;
        }
   

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_dbConnection != null)
                {
                    _dbConnection.Dispose();
                    _dbConnection = null;
                }
                if (_dbCommand != null)
                {
                    _dbCommand.Dispose();
                    _dbCommand = null;
                }
                if (_dbTransaction != null)
                {
                    _dbTransaction.Dispose();
                    _dbTransaction = null;
                }
                if (_dbDataReader != null)
                {
                    _dbDataReader.Dispose();
                    _dbDataReader = null;
                }
                if (_detachedQuery != null)
                {
                    _detachedQuery.Dispose();
                    _detachedQuery = null;
                }
                _dbProviderFactory = null;
                _outputParameters.Clear();
                _outputParameters = null;
                _parameterBuilder = null;
                _dbParameterList.Clear();
                _dbParameterList = null;
                _escapeIdentifier = null;

                
            }

        }
        ~DataService()
        {
            Dispose(false);
        }

        /// <summary>
        /// Select işlemi sonucu dönebilecek olan maksimum satır sayısını gösterir. Satır sayısı 100.000 satır ile sınırlandırılmıştır.
        /// </summary>
        public int MaxNumberOfResults
        {
            get
            {
                return _maxNumberOfResults;
            }
            set
            {
                _maxNumberOfResults = value;
            }
        }

        protected DbCommand DbCommand { get { return _dbCommand; } }
     
        /// <summary>
        /// Veritabanı üzerinde çalıştırılan prosedür veya fonksiyonlara verilen output parametrelerin adını ve değerini döndürür.
        /// </summary>
        public Dictionary<string, object> OutputParameters
        {
            get
            {
                if (_outputParameters.Count < 1)
                {
                    foreach (DbParameter item in _dbCommand.Parameters)
                    {
                        if (item.Direction == ParameterDirection.Output || item.Direction == ParameterDirection.InputOutput)
                            _outputParameters.Add(item.ParameterName, item.Value);
                    }
                }

                return _outputParameters;
            }
        }

       /// <summary>
        /// <see cref="DataTable"/> ile çalışmak için gerekli metodları sağlar.
       /// </summary>
        public IDetachedQuery DetachedQuery 
        { 
            get 
            {
                if (_detachedQuery == null)
                    _detachedQuery = new DetachedQuery(this,_serviceConnectionString);

                return _detachedQuery;
            }
        }

        protected internal void OpenConnection()
        {
            if (_dbConnection == null)
            {
                _dbConnection = CreateConnection(_serviceConnectionString);
                _dbCommand = CreateCommand(_dbConnection);
                _dbConnection.Open();
            }
            else if (_dbConnection.State == ConnectionState.Closed)
            {
                _dbConnection.Open();
            }
            _outputParameters.Clear();
        }

        protected internal void CloseConnection()
        {
            if (_dbTransaction == null)
            {
                if (_dbConnection != null)
                {
                    _dbConnection.Close();
                }
            }

            ClearParameters();
        }

        internal DbConnection CreateConnection(string connectionString)
        {
            if (string.IsNullOrWhiteSpace(connectionString))
                throw new ArgumentNullException("connectionString");

            var conn = _dbProviderFactory.CreateConnection();
            conn.ConnectionString = connectionString;
            return conn;
        }

        internal  DbCommand CreateCommand(DbConnection connection)
        {
            if (connection == null)
                throw new ArgumentNullException("DbConnection");

            var _cmd = connection.CreateCommand();

            PropertyInfo property = _cmd.GetType().GetProperty("BindByName");
            if (property != null)
            {
                property.SetValue(_cmd, true, null);
            }

            return _cmd;
        }        

        internal void AddCommandParameters()
        {
            foreach (DbParameter item in _dbParameterList)
            {
                if (item.Value == null)
                    item.Value = DBNull.Value;

                _dbCommand.Parameters.Add(item);
            }           
        }

        internal void ClearParameters()
        {
            if (_dbCommand != null)
                _dbCommand.Parameters.Clear();

            _dbParameterList.Clear();
        }

        /// <summary>
        /// Transaction işlemini başlatır
        /// </summary>
        public void BeginTransaction()
        {
            if (_dbTransaction == null)
            {
                OpenConnection();
                _dbTransaction = _dbConnection.BeginTransaction(System.Data.IsolationLevel.ReadCommitted);
                _dbCommand.Transaction = _dbTransaction;
            }
        }


        /// <summary>
        /// Transaction işlemini tamamlar.
        /// </summary>
        public void CommitTransaction()
        {
            if (_dbTransaction != null)
            {
                _dbTransaction.Commit();
                _dbConnection.Close();
                _dbTransaction.Dispose();
                _dbTransaction = null;
            }
            ClearParameters();
        }

        /// <summary>
        /// Transaction işlemini iptal eder.
        /// </summary>
        public void RollbackTransaction()
        {
            if (_dbTransaction != null)
            {
                _dbTransaction.Rollback();
                _dbConnection.Close();
                _dbTransaction.Dispose();
                _dbTransaction = null;
            }
            ClearParameters();
        }

        #region CRUD işlemleri

        /// <summary>
        /// Verilen sınıfa göre select işlemi yapar.
        /// </summary>
        /// <typeparam name="T">İşlem yapılacak olan tabloya ait sınıf</typeparam>
        /// <param name="commandText">Select cümlesi. Eğer bu parametre boş ise <c> "select * from"</c> işlemi yapılır fakat sorgu sonucuna ait ilk satır gösterilir.</param>
        /// <param name="commandType">Komut tipi.</param>
        /// <returns>Select işleminin sonucu</returns>
        public  T FindBy<T>(string commandText = "", CommandType commandType = CommandType.Text)
        {
            TableInfo table = EntityQueryCache.GetCachedTableInfo(typeof(T), _parameterBuilder, _escapeIdentifier,_dbProviderFactory);

            ICommandStringBuilder selectCommand = new SelectCommandBuilder();

            string _cmdText;

            if (!string.IsNullOrWhiteSpace(commandText))//commandText boş değilse
            {
                if (commandText.Trim().StartsWith("where", StringComparison.InvariantCultureIgnoreCase))//eğer commandText where ile başlıyorsa
                {
                    _cmdText = selectCommand.CommandString(string.Format("select * from {0} {1}", table.TableName, commandText)).ToString();//select cümlesini başa ekle
                }
                else
                {
                    _cmdText = selectCommand.CommandString(commandText).ToString();
                }
            }
            else//eğer commandText boşsa "select * from" yap
            {
                _cmdText = selectCommand.CommandString(string.Format("select * from {0} ", table.TableName)).ToString();
            }

            OpenConnection();
           
            _dbCommand.CommandText = _cmdText;
            _dbCommand.CommandType = commandType;
            T item= default(T);

            try
            {
                AddCommandParameters();
                _dbDataReader = _dbCommand.ExecuteReader(CommandBehavior.SingleRow);//sorgu sonucu sadece tek satır getir.
                
                DynamicEntityBuilder<T> _builder = DynamicEntityBuilder<T>.CreateBuilder(_dbDataReader);                

                if (_dbDataReader.Read())
                    item = _builder.Build(_dbDataReader);               
            }
            finally
            {
                _dbDataReader.Close();
                CloseConnection();
            }

            
            return item;

        }

        /// <summary>
        /// Verilen sınıfa göre select işlemi yapar.
        /// </summary>
        /// <typeparam name="T">İşlem yapılacak olan tabloya ait sınıf</typeparam>
        /// <param name="commandText">Select cümlesi. Eğer bu parametre boş ise <c> "select * from"</c> işlemi yapılır fakat sorgu sonucuna ait ilk satır gösterilir.</param>
        /// <param name="commandType">Komut tipi.</param>
        /// <returns>Select işleminin sonucu. Bu işlem sonucunda var sayılan olarak 100.000 satır getirilir.</returns>
        public  IEnumerable<T> FindAll<T>(string commandText = "", CommandType commandType = CommandType.Text)
        {
            TableInfo table = EntityQueryCache.GetCachedTableInfo(typeof(T), _parameterBuilder, _escapeIdentifier,_dbProviderFactory);

            ICommandStringBuilder selectCommand = new SelectCommandBuilder();

            string _cmdText;
            if (!string.IsNullOrWhiteSpace(commandText))//commandText boş değilse
            {
                if (commandText.Trim().StartsWith("where", StringComparison.InvariantCultureIgnoreCase))//eğer commandText where ile başlıyorsa
                {
                    _cmdText = selectCommand.CommandString(string.Format("select * from {0} {1} ", table.TableName, commandText)).ToString();//select cümlesini başa ekle
                }
                else
                {
                    _cmdText = selectCommand.CommandString(commandText).ToString();
                }
            }
            else//eğer commandText boşsa "select * from" yap
            {
                _cmdText = selectCommand.CommandString(string.Format("select * from {0}  ", table.TableName)).ToString();
            }

            OpenConnection();
           
            _dbCommand.CommandText = _cmdText;
            _dbCommand.CommandType = commandType;
            IList<T> _items = new List<T>(32);

            try
            {
                AddCommandParameters();
                _dbDataReader = _dbCommand.ExecuteReader(CommandBehavior.CloseConnection);
                DynamicEntityBuilder<T> _builder = DynamicEntityBuilder<T>.CreateBuilder(_dbDataReader);               
                long maxRowSize = 0;
                //veritabanından çekilecek olan maximum satır sayısı _maxNumberOfResults ile sınırlanmıştır.Bu sınırı aşma.
                while (_dbDataReader.Read() & _maxNumberOfResults >= maxRowSize)
                {
                    _items.Add(_builder.Build(_dbDataReader));
                    System.Threading.Interlocked.Increment(ref maxRowSize);
                }
            }
            finally
            {
                _dbDataReader.Close();
                CloseConnection();
            }

           
            return _items;

        }

        
        //internal  void ExpressionTest(Type item)
        //{
        //   //var sonuc=MethodCache<T>.GetLambda()(item, _parameterBuilder, _escapeIdentifier);
        //   // var sonuc = EntityQueryCache<T>.Instance.GetTableQueriesAndInfo(item, _parameterBuilder, _escapeIdentifier);            
        //    var sonuc = TableInfoCache.Instance.GetTableInfo(item, _parameterBuilder, _escapeIdentifier);
        //    //var tip = ClassDecomposer2.CreateTableInfoFromType();
        //    //var sonuc2 = tip(item.GetType(), _parameterBuilder, _escapeIdentifier);
        //}


        //internal void ReflectionTest(Type item)
        //{
        //    TableInfo table = ClassDecomposer.ForType(item, _parameterBuilder, _escapeIdentifier);
        //}

        /// <summary>
        /// Insert işlemi yapar
        /// </summary>
        /// <typeparam name="T">Eklenecek sınıf</typeparam>
        /// <param name="item">Eklenecek sınıf</param>
        /// <returns>Etkilenen satır sayısı.</returns>
        public  int Insert<T>(T item)
        {
            if (item is string)
                return  Insert(item.ToString(), CommandType.Text);

            var info = EntityQueryCache.GetCachedTableInfo(item, _parameterBuilder, _escapeIdentifier, _dbProviderFactory);
            OpenConnection();

            _dbCommand.CommandText = info.TableSqlCommands.InsertQuery;
            int result;

            foreach (var column in info.Properties)
            {
               
                if(!column.AutoIncrement )
                {                  
                    column.ParameterInfo.DatabaseParameter.Value = ParameterInfo.GetParameterValue(column.CurrentPropertyDetails, item) ?? DBNull.Value;

                    _dbCommand.Parameters.Add(column.ParameterInfo.DatabaseParameter);
                }
            }

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }
            
            
            return result;
        }

        /// <summary>
        /// Insert işlemi yapar
        /// </summary>
        /// <param name="commandText">Ekleme işlemi için kullanılacak komut.</param>
        /// <param name="commandType">Komut tipi.</param>
        ///<returns>Etkilenen satır sayısı.</returns>
        public  int Insert(string commandText, CommandType commandType = CommandType.Text)
        {
            if (string.IsNullOrWhiteSpace(commandText))
                throw new ArgumentNullException("commandText");

            OpenConnection();
           
            _dbCommand.CommandText = commandText;
            _dbCommand.CommandType = commandType;
            AddCommandParameters();
            int result;

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }
           
            return result;
        }

        /// <summary>
        /// Update işlemi yapar
        /// </summary>
        /// <typeparam name="T">Güncellenecek sınıf.</typeparam>
        /// <param name="item">Güncellenecek sınıf.</param>
        /// <returns>Etkilenen satır sayısı.</returns>
        public  int Update<T>(T item)
        {

            if (item is string)
                return  Update(item.ToString(), CommandType.Text);

            var info = EntityQueryCache.GetCachedTableInfo(item, _parameterBuilder, _escapeIdentifier,_dbProviderFactory);

            OpenConnection();
            _dbCommand.CommandText = info.TableSqlCommands.UpdateQuery;

            foreach (var column in info.Properties)
            {
                column.ParameterInfo.DatabaseParameter.Value = ParameterInfo.GetParameterValue(column.CurrentPropertyDetails, item) ?? DBNull.Value;

                _dbCommand.Parameters.Add(column.ParameterInfo.DatabaseParameter);

            }

            int result;

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }           
            
            return result;
        }

        /// <summary>
        /// Update işlemi yapar
        /// </summary>
        /// <param name="commandText">Güncelleme işlemi için kullanılacak komut.</param>
        /// <param name="commandType">Komut tipi.</param>
        ///<returns>Etkilenen satır sayısı.</returns>
        public int Update(string commandText, CommandType commandType = CommandType.Text)
        {
            if (string.IsNullOrWhiteSpace(commandText))
                throw new ArgumentNullException("commandText");

            OpenConnection();
            AddCommandParameters();
            _dbCommand.CommandText = commandText;
            _dbCommand.CommandType = commandType;

            int result;

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }

            return result;
        }

        /// <summary>
        /// Delete işlemi yapar
        /// </summary>
        /// <typeparam name="T">Silincek sınıf</typeparam>
        /// <param name="item">Silinecek sınıf</param>
        /// <returns>Etkilenen satır sayısı.</returns>
        public int Delete(string commandText, CommandType commandType = CommandType.Text)
        {
            if (string.IsNullOrWhiteSpace(commandText))
                throw new ArgumentNullException("commandText");

            OpenConnection();
            AddCommandParameters();
            _dbCommand.CommandText = commandText;
            _dbCommand.CommandType = commandType;

            int result;

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }

            return result;

        }

        /// <summary>
        /// Delete işlemi yapar
        /// </summary>
        /// <param name="commandText">Silme işlemi için kullanılacak komut.</param>
        /// <param name="commandType">Komut tipi.</param>
        ///<returns>Etkilenen satır sayısı.</returns>
        public int Delete<T>(T item)
        {

            if (item is string)
                return Delete(item.ToString(), CommandType.Text);

            var info = EntityQueryCache.GetCachedTableInfo(item, _parameterBuilder, _escapeIdentifier, _dbProviderFactory);

            OpenConnection();
            _dbCommand.CommandText = info.TableSqlCommands.DeleteQuery;

            foreach (var column in info.Properties)
            {
                if (column.PrimaryKey)
                {
                    column.ParameterInfo.DatabaseParameter.Value = ParameterInfo.GetParameterValue(column.CurrentPropertyDetails, item) ?? DBNull.Value;

                    _dbCommand.Parameters.Add(column.ParameterInfo.DatabaseParameter);
                }

            }

            int result;

            try
            {
                result = _dbCommand.ExecuteNonQuery();
            }
            finally
            {
                CloseConnection();
            }

            return result;
        }


        /// <summary>
        /// Sum, Count gibi tek değer üreten sorgular için kullanılır.
        /// </summary>
        /// <param name="commandText">Kullanılacak komut.</param>
        /// <param name="commandType">Komut tipi.</param>
        /// <returns>Sorgu sonucu.</returns>
        public  object GetScalarValue(string commandText, CommandType commandType = CommandType.Text)
        {
            if (string.IsNullOrWhiteSpace(commandText))
                throw new ArgumentNullException("commandText");

            OpenConnection();
            AddCommandParameters();
            _dbCommand.CommandText = commandText;
            _dbCommand.CommandType = commandType;

            object result;

            try
            {
                result = _dbCommand.ExecuteScalar();
            }
            finally
            {
                CloseConnection();
            }

            return result;
        }

        /// <summary>
        /// Sorgu sonucunu sayfalamak için kullanılır.
        /// </summary>
        /// <typeparam name="T">Sorgu için kullanılacak sınıf.</typeparam>
        /// <param name="currentPage">Sayfa numarası.</param>
        /// <param name="pageSize">Bir sayfada gösterilecek olan satır sayısı.</param>
        /// <param name="whereCommand">Where komutu.</param>
        /// <param name="orderByCommand">Order by komutu.</param>
        /// <returns><see cref="PagedResults"/> sınıfı içine konulmuş sorgu sonucu </returns>
        public abstract PagedResults<T> Page<T>(int currentPage = 1, int pageSize = 20, string whereCommand = "", string orderByCommand = "");
       
        #endregion

        /// <summary>
        /// Input tipinden parametre eklemek için kullanılır.
        /// </summary>
        /// <param name="parameterName">Parametre adı.</param>
        /// <param name="parameterValue">Parametre değeri</param>
        /// <param name="dataType">Parametrenin veritabanı tipi</param>
        public virtual void AddInputParameter(string parameterName, object parameterValue, DbType dbType)
        {
            DbParameter p = _dbProviderFactory.CreateParameter();
            p.DbType = dbType;
            p.ParameterName = parameterName;
            p.Value = parameterValue ?? DBNull.Value;
            _dbParameterList.Add(p);           
        }

     

        /// <summary>
        /// Output tipinden parametre eklemek için kullanılır.
        /// </summary>
        /// <param name="parameterName">Parametre adı.</param>
        /// <param name="dataType">Parametrenin veritabanı tipi</param>
        /// <param name="parameterSize">Parametrenin veritabanı sütununda kapladığı alan.Bu değer int ,double gibi tiplerde sıfırdır.</param>
        public virtual void AddOutputParameter(string parameterName, DbType dbType, int parameterSize)
        {
            DbParameter p = _dbProviderFactory.CreateParameter();
            p.DbType = dbType;
            p.ParameterName = parameterName;

            if (parameterSize >= 0)
                p.Size = parameterSize;

            p.Direction = ParameterDirection.Output;
            _dbParameterList.Add(p);
        }

       

        /// <summary>
        /// Input Output tipinden parametre eklemek için kullanılır.
        /// </summary>
        /// <param name="parameterName">Parametre adı.</param>
        /// <param name="parameterValue">Parametre değeri</param>
        /// <param name="dataType">Parametrenin veritabanı tipi</param>
        /// <param name="parameterSize">Parametrenin veritabanı sütununda kapladığı alan.Bu değer int ,double gibi tiplerde sıfırdır.</param>
        public virtual void AddInputOutputParameter(string parameterName, object parameterValue, DbType dbType, int parameterSize)
        {
            DbParameter p = _dbProviderFactory.CreateParameter();
            p.DbType = dbType;
            p.ParameterName = parameterName;
            p.Value = parameterValue ?? DBNull.Value;
            if (parameterSize >= 0)
                p.Size = parameterSize;

            p.Direction = ParameterDirection.InputOutput;
            _dbParameterList.Add(p);
        }

    


     
    }
}
